/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.itcast.activiti.service.impl;


import cn.itcast.ic.common.bean.CommonErrorInfo;
import cn.itcast.ic.common.bean.Response;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.itcast.activiti.constant.CategoryConstant;
import com.itcast.activiti.constant.NodeKeyTag;
import com.itcast.activiti.domain.StartProcessInstance;
import com.itcast.activiti.entity.ApproveDefinition;
import com.itcast.activiti.service.IDeployRepositoryService;
import com.itcast.activiti.utils.ActivitiPage;
import com.itcast.activiti.utils.TimeUtils;
import com.itcast.activiti.utils.UUIDUtils;
import lombok.extern.slf4j.Slf4j;
import org.activiti.bpmn.BpmnAutoLayout;
import org.activiti.bpmn.model.Process;
import org.activiti.bpmn.model.*;
import org.activiti.engine.RepositoryService;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.repository.DeploymentQuery;
import org.activiti.engine.repository.ProcessDefinition;
import org.activiti.engine.repository.ProcessDefinitionQuery;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 提供对流程定义和部署存储库的访问的服务
 */
@Slf4j
@Service
public class IDeployRepositoryServiceImpl implements IDeployRepositoryService {
    @Autowired
    private RepositoryService repositoryService;


    @Override
    public String createDeploymentJson(ApproveDefinition approveDefinition) {
        String definitionId = approveDefinition.getId();
        String name = approveDefinition.getName();
        String tenantId = approveDefinition.getTenantId();
        String flowJsonStr = approveDefinition.getFlowJson();
        Assert.hasText(definitionId, "definitionId is empty");
        Assert.hasText(tenantId, "tenantId is empty");
        Assert.hasText(name, "name is empty");
        // processKey使用流程定义表的ID
        String processKey = NodeKeyTag.PROCESS_PREFIX + definitionId;
        //创建bpmn模型
        BpmnModel model = new BpmnModel();
        Process process = new Process();
        process.setName(name);
        model.addProcess(process);
        process.setId(processKey);
        // 将json字符串转为json数组
        JSONArray flowJson = JSONObject.parseArray(flowJsonStr);
        if (flowJson == null || flowJson.size() == 0) Assert.notNull(null, "解析流程json为空！");
        if (!"start".equals(((JSONObject) flowJson.get(0)).getString("type")))
            Assert.notNull(null, "解析流程json出错！第一个节点不是start节点！");
        if (!"end".equals(((JSONObject) flowJson.get(flowJson.size() - 1)).getString("type")))
            Assert.notNull(null, "解析流程json出错！最后一个节点不是end节点！");
        // 生成process中的节点
        log.info("流程json {}", flowJsonStr);
        flowNodeArrayToActiviti(process, null, flowJson, null, null);
        // 生成BPMN自动布局，即 model 中各节点的位置
        new BpmnAutoLayout(model).execute();
        // 执行部署操作
        Deployment deployment = repositoryService.createDeployment()
                                                 .addBpmnModel(definitionId + ".bpmn", model)
                                                 .name(name)
                                                 .key(definitionId)
                                                 .tenantId(tenantId)
                                                 .deploy();
        Assert.notNull(deployment, "create deployment fail");
        log.info("流程部署成功响应 {}", JSONArray.toJSON(createDeploymentResultMap(deployment)));
        return deployment.getId();
    }

    // 结束节点
    private JSONObject nextEndNode = null;

    /**
     * 生成process中的节点
     * 中间节点，只有 CCList：只会类型）、approval：审批类型）、cond：条件类型） 三种类型
     * 中间节点，只有 CCList：只会类型）、approval：审批类型）、condition：条件类型） 三种类型
     *
     * @param process    流程节点对象
     * @param prevOutArr 上个节点
     * @param flowJson   流程json
     * @param nextOutArr 下个节点
     * @param condition  条件节点
     */
    private void flowNodeArrayToActiviti(Process process, JSONObject prevOutArr, JSONArray flowJson, JSONObject nextOutArr, JSONObject condition) {
        for (int j = 0; j < flowJson.size(); j++) {
            JSONObject prevNode = null, currentNode = null, nextNode = null;
            // 当前节点
            currentNode = (JSONObject) flowJson.get(j);
            // 条件判断节点，该节点不参与生成工作流节点
            if ("cond".equals(currentNode.getString("type")))
                continue;
            if (j > 0 && !"cond".equals(((JSONObject) flowJson.get(j - 1)).getString("type"))) {
                // 上个节点
                prevNode = (JSONObject) flowJson.get(j - 1);
            }
            if (j < flowJson.size() - 1) {
                // 下个节点
                nextNode = (JSONObject) flowJson.get(j + 1);
                if (!nextNode.containsKey("type")) {
                    Assert.notNull(null, nextNode.getString("id") + "：节点缺少 type");
                }
                if (nextNode.getString("type").equals("end")) {
                    if (nextEndNode == null) {
                        nextEndNode = (JSONObject) flowJson.get(j + 1);
                    }
                }
            }
            // 将自定义的JSON对象转为activiti的流程格式
            flowNodeObjectToActiviti(process, prevNode, prevOutArr, currentNode, nextNode, nextOutArr, condition);
        }
    }

    /**
     * 将自定义的JSON对象转为activiti的流程格式
     *
     * @param process     流程节点对象
     * @param prevNode    上个节点
     * @param prevOutArr  上个节点
     * @param currentNode 当前节点
     * @param nextInArr   下个节点
     * @param nextOutArr  用户节点出口线只有一条，有数组内下个节点则优先连接，否则再判断数组外是否有下个节点再连接
     * @param condition   条件节点
     */
    private void flowNodeObjectToActiviti(Process process, JSONObject prevNode, JSONObject prevOutArr, JSONObject currentNode, JSONObject nextInArr, JSONObject nextOutArr, JSONObject condition) {
        if ("condition".equals(currentNode.getString("type"))) {
            // 新建排他网关
            newExclusiveGateWay(process, currentNode);
            // 如果prevOutArr是网关，并且prevInArr为空，说明当前节点是网关的条件分支节点的第一个节点，生成网关到当前节点的连接线
            if (prevNode == null && prevOutArr != null && "condition".equals(prevOutArr.getString("type"))) {
                newProcessSequenceFlow(process, prevOutArr, currentNode, condition);
            }
            // 循环cond条件前，prevInArr指向当前元素
            prevNode = currentNode;
            JSONArray conds = (JSONArray) currentNode.get("node");
            if (conds == null || conds.size() == 0)
                Assert.notNull(null, "未获取到条件判断下的具体分支：" + currentNode.getString("id"));
            // 循环遍历条件分支：第一个节点必然是cond类型，将其转化为流程线即可，不做任何处理
            for (int i = 0; i < conds.size(); i++) {
                JSONArray nodes = (JSONArray) conds.get(i);
                JSONObject condOne = (JSONObject) nodes.get(0);
                if (nodes == null || nodes.size() == 0)
                    Assert.notNull(null, "未获取到条件判断下的具体分支：" + currentNode.getString("id"));
                // 遍历数组
                flowNodeArrayToActiviti(process, prevNode, nodes, nextInArr, condOne);
            }
        } else if (Arrays.asList("CCList", "approval").contains(currentNode.getString("type"))) {
            // 新建用户节点
            newProcessUserTaskNode(process, currentNode);
            // 如果prevOutArr是网关，并且prevInArr为空，说明当前节点是网关的条件分支节点的第一个节点，生成网关到当前节点的连接线
            if (prevNode == null && prevOutArr != null && "condition".equals(prevOutArr.getString("type"))) {
                newProcessSequenceFlow(process, prevOutArr, currentNode, condition);
            }
            // 用户节点出口线只有一条，有数组内下个节点则优先连接，否则再判断数组外是否有下个节点再连接
            if (nextInArr != null) {
                newProcessSequenceFlow(process, currentNode, nextInArr);
            } else if (nextOutArr != null) {
                newProcessSequenceFlow(process, currentNode, nextOutArr);
            } else if (nextInArr == null && nextOutArr == null) {
                // 嵌套条件连线结束节点
                newProcessSequenceFlow(process, currentNode, nextEndNode);
            }
        } else if ("start".equals(currentNode.getString("type"))) {
            // 开始节点
            newProcessStartEvent(process, currentNode);
            // 开始节点出口线只有一条，有数组内下个节点则优先连接，否则再判断数组外是否有下个节点再连接
            if (nextInArr != null) {
                newProcessSequenceFlow(process, currentNode, nextInArr);
            } else if (nextOutArr != null) {
                newProcessSequenceFlow(process, currentNode, nextOutArr);
            }
        } else if ("end".equals(currentNode.getString("type"))) {
            // 结束节点
            newProcessEndEvent(process, currentNode);
            // 如果prevOutArr是网关，并且prevInArr为空，说明当前节点是网关的条件分支节点的第一个节点，生成网关到当前节点的连接线
            if (prevNode == null && prevOutArr != null && "condition".equals(prevOutArr.getString("type"))) {
                newProcessSequenceFlow(process, prevOutArr, currentNode, condition);
            }
        } else if ("cond".equals(currentNode.getString("type"))) {
            // 条件判断节点，该节点不参与生成工作流节点
        } else {
            throw new RuntimeException("解析根节点下节点类型出错：" + currentNode.getString("type"));
        }
    }

    // 生成网关
    private void newExclusiveGateWay(Process process, JSONObject curr) {
        // 生成一个排他网关
        Gateway gateway = new ExclusiveGateway();
        gateway.setId(getElementIdByJsonObject(curr));
        gateway.setName("条件判断");
        process.addFlowElement(gateway);
    }

    // 依据 JSONObject 生成bpmn元素的ID
    private String getElementIdByJsonObject(JSONObject node) {
        return getElementIdByJsonType(node.getString("type"), node.getString("nodeKey"));
    }

    // 依据 JSONObject 生成bpmn元素的ID
    private String getElementIdByJsonType(String type, String nodeKey) {
        if ("condition".equals(type)) {
            return NodeKeyTag.GATEWAY_PREFIX + nodeKey;
        } else if (Arrays.asList("CCList", "approval").contains(type)) {
            return NodeKeyTag.USERTASK_PREFIX + nodeKey;
        } else if ("start".equals(type)) {
            return NodeKeyTag.START_PREFIX + nodeKey;
        } else if ("end".equals(type)) {
            return NodeKeyTag.END_PREFIX + nodeKey;
        }
        return null;
    }

    /**
     * 生成流程线
     *
     * @param process 流程节点对象
     * @param source  上一个节点
     * @param target  当前节点
     * @param cond    条件节点
     * @return
     */
    private SequenceFlow newProcessSequenceFlow(Process process, JSONObject source, JSONObject target, JSONObject cond) {
        String conditionExpression = null;
        if (cond != null) {
            // 网关条件
            conditionExpression = cond.getString("conditionExpression");
            Assert.hasText(conditionExpression, "条件表达式为空");
        }
        SequenceFlow flow = newSequenceFlow(getElementIdByJsonObject(source), getElementIdByJsonObject(target), conditionExpression);
        process.addFlowElement(flow);
        return flow;
    }

    /**
     * 生成流程线
     *
     * @param process 流程节点对象
     * @param source  上一个节点
     * @param target  当前节点
     * @return
     */
    private SequenceFlow newProcessSequenceFlow(Process process, JSONObject source, JSONObject target) {
        SequenceFlow flow = newSequenceFlow(getElementIdByJsonObject(source), getElementIdByJsonObject(target), null);
        process.addFlowElement(flow);
        return flow;
    }

    /**
     * 生成流程线
     *
     * @param sourceId            上一个节点id
     * @param targetId            当前节点id
     * @param conditionExpression 排他网关,条件表达式
     * @return
     */
    private SequenceFlow newSequenceFlow(String sourceId, String targetId, String conditionExpression) {
        SequenceFlow flow = new SequenceFlow();
        flow.setSourceRef(sourceId);
        flow.setTargetRef(targetId);
        flow.setId(NodeKeyTag.SEQUENCEFLOW_PREFIX + UUIDUtils.randomUuid());
        if (!StringUtils.isEmpty(conditionExpression)) {
            flow.setConditionExpression(conditionExpression);
        }
        return flow;
    }

    // 生成用户节点
    private void newProcessUserTaskNode(Process process, JSONObject curr) {
        // multiApproveMode：1会签，2或签
        process.addFlowElement(newUserTask(curr.getString("name"), curr.getString("nodeKey"), curr.getString("type"), curr.getString("multiApproveMode")));
    }

    // 产生一个UserTask
    // 多人审批时采用的审批方式 :multiApproveMode：1会签，2或签
    private UserTask newUserTask(String name, String nodeKey, String nodeType, String multiApproveMode) {
        UserTask userTask = new UserTask();
        userTask.setName(name);
        userTask.setId(getElementIdByJsonType(nodeType, nodeKey));
        if ("approval".equals(nodeType)) {
            userTask.setCategory("1"); // 审批
            // 审批节点，默认是或签模式，只要有一个人审核，则该节点通过。如果需要会签模式，则需要设置多任务并行。
            // 会签
            if ("1".equals(multiApproveMode)) {
                // 多任务并行
                MultiInstanceLoopCharacteristics loopCharacteristics = new MultiInstanceLoopCharacteristics();
                loopCharacteristics.setSequential(false); // 并行
                loopCharacteristics.setInputDataItem("${" + NodeKeyTag.USERS_PREFIX + nodeKey + "}");
                loopCharacteristics.setElementVariable("assignee");
                userTask.setLoopCharacteristics(loopCharacteristics);
                userTask.setAssignee("${assignee}");
            }
            // 或签
            else {
                // 或签通过候选组实现。多个人任意一个人处理，则该节点完成并跳转下节点
                userTask.setCandidateUsers(Arrays.asList("${" + NodeKeyTag.USERS_PREFIX + nodeKey + "}"));
            }
        } else if ("CCList".equals(nodeType)) {
            // 知会
            userTask.setCategory("2");
        } else {
            throw new RuntimeException("生成用户节点时类型错误！" + nodeType);
        }
        return userTask;
    }

    // 生成开始节点
    private void newProcessStartEvent(Process process, JSONObject curr) {
        StartEvent startEvent = new StartEvent();
        startEvent.setId(getElementIdByJsonObject(curr));
        startEvent.setName(curr.getString("name"));
        process.addFlowElement(startEvent);
    }

    // 生成结束节点
    private void newProcessEndEvent(Process process, JSONObject curr) {
        EndEvent endEvent = new EndEvent();
        endEvent.setId(getElementIdByJsonObject(curr));
        endEvent.setName(curr.getString("name"));
        process.addFlowElement(endEvent);
    }
    // ****************** 上方是JSON方式创建新的部署 *********************************************************************************************

    @Override
    public Response<Map<String, Object>> createDeploymentFile(MultipartFile file, String key, String tenantId) {
        String originalFilename = file.getOriginalFilename();
        try {
            Deployment deployment = repositoryService.createDeployment()
                                                     .addBytes(originalFilename, file.getBytes())
                                                     .key(StringUtils.hasText(key) ? key : UUID.randomUUID().toString())
                                                     .name(originalFilename)
                                                     .tenantId(tenantId)
                                                     .deploy();
            Assert.notNull(deployment, "create deployment fail");
            return Response.success(createDeploymentResultMap(deployment));
        } catch (IOException e) {
            e.printStackTrace();
            return Response.error(CommonErrorInfo.unkownError(e.getMessage()));
        }
    }

    @Override
    public void getBpmnFile(String deploymentId, String processDefinitionId, String processDefinitionKey, ServletOutputStream outputStream) {
        if (!StringUtils.hasText(deploymentId) && !StringUtils.hasText(processDefinitionId) && !StringUtils.hasText(processDefinitionKey)) {
            Assert.notNull(null, "param is empty");
        }
        ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
        if (StringUtils.hasText(deploymentId)) {
            processDefinitionQuery.deploymentId(deploymentId);
        }
        if (StringUtils.hasText(processDefinitionId)) {
            processDefinitionQuery.processDefinitionId(processDefinitionId);
        }
        if (StringUtils.hasText(processDefinitionKey)) {
            processDefinitionQuery.processDefinitionKey(processDefinitionKey);
        }
        try {
            ProcessDefinition processDefinition = processDefinitionQuery.singleResult();
            Assert.notNull(processDefinition, "process definition is empty");
            InputStream imageStreamBpmn = repositoryService.getResourceAsStream(processDefinition.getDeploymentId(), processDefinition.getResourceName());
            try {
                IOUtils.copy(imageStreamBpmn, outputStream);
            } catch (IOException e) {
                e.printStackTrace();
            }
            imageStreamBpmn.close();
            outputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public ActivitiPage listDeploymentPage(ActivitiPage activitiPage) {
        JSONObject condition = activitiPage.getCondition();
        DeploymentQuery deploymentQuery = repositoryService.createDeploymentQuery().orderByDeploymenTime().desc();
        if (StringUtils.hasText(condition.getString(StartProcessInstance.Fields.processDefinitionKey))) {
            deploymentQuery.processDefinitionKey(condition.getString(StartProcessInstance.Fields.processDefinitionKey));
        }
        Integer startIndex = Math.toIntExact(activitiPage.getCurrent());
        Integer maxItems = Math.toIntExact(activitiPage.getSize());
        List<Deployment> deploymentList = deploymentQuery.listPage(startIndex, maxItems);
        List<Map<String, Object>> resultList = new ArrayList<>();
        for (Deployment deployment : deploymentList) {
            Map<String, Object> deploymentResultMap = createDeploymentResultMap(deployment);
            resultList.add(deploymentResultMap);
        }
        activitiPage.setTotal(deploymentQuery.count());
        activitiPage.setRecords(resultList);
        return activitiPage;
    }

    @Override
    public ActivitiPage checkProcessDefinitionKey(String processDefinitionKey) {
        ActivitiPage activitiPage = new ActivitiPage();
        JSONObject condition = new JSONObject();
        condition.put(StartProcessInstance.Fields.processDefinitionKey, processDefinitionKey);
        activitiPage.setCondition(condition);
        return listDeploymentPage(activitiPage);
    }

    @Override
    public ActivitiPage listProcessDefinitionPage(ActivitiPage activitiPage) {
        JSONObject condition = activitiPage.getCondition();
        ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery().orderByProcessDefinitionVersion().desc();
        if (StringUtils.hasText(condition.getString(StartProcessInstance.Fields.processDefinitionKey))) {
            processDefinitionQuery.processDefinitionKey(condition.getString(StartProcessInstance.Fields.processDefinitionKey));
        }
        Integer startIndex = Math.toIntExact(activitiPage.getCurrent());
        Integer maxItems = Math.toIntExact(activitiPage.getSize());
        List<ProcessDefinition> processDefinitionList = processDefinitionQuery.listPage(startIndex, maxItems);
        List<Map<String, Object>> resultList = new ArrayList<>();
        int signCount = 0;
        for (ProcessDefinition processDefinition : processDefinitionList) {
            if (CategoryConstant.ADD_REMOVE_SIGN.equals(processDefinition.getDescription())) {
                signCount++;
                continue;
            }
            Map<String, Object> resultMap = new HashMap<>();
            resultMap.put("id", processDefinition.getId());
            resultMap.put("version", processDefinition.getVersion());
            resultMap.put("name", processDefinition.getName());
            resultMap.put("processDefinitionKey", processDefinition.getKey());
            resultMap.put("deploymentId", processDefinition.getDeploymentId());
            resultMap.put("resourceName", processDefinition.getResourceName());
            resultMap.put("riagramResourceName", processDefinition.getDiagramResourceName());
            resultMap.put("engineVersion", processDefinition.getEngineVersion());
            resultMap.put("tenantId", processDefinition.getTenantId());
            resultList.add(resultMap);
        }
        activitiPage.setTotal(processDefinitionQuery.count() - signCount);
        activitiPage.setRecords(resultList);
        return activitiPage;
    }

    @Override
    public BpmnModel getBpmnModelByProcessDefinitionId(String processDefinitionId) {
        return repositoryService.getBpmnModel(processDefinitionId);
    }

    private Map<String, Object> createDeploymentResultMap(Deployment deployment) {
        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("id", deployment.getId());
        resultMap.put("deploymentTime", TimeUtils.format(deployment.getDeploymentTime()));
        resultMap.put("name", deployment.getName());
        resultMap.put("tenantId", deployment.getTenantId());
        resultMap.put("key", deployment.getKey());
        return resultMap;
    }

    @Override
    public List<String> listProcessDefinitionId(Set<String> processDefinitionKeys) {
        ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery().orderByProcessDefinitionVersion().desc();
        processDefinitionQuery.processDefinitionKeys(processDefinitionKeys);
        List<ProcessDefinition> list = processDefinitionQuery.list();
        return list.stream().map(ProcessDefinition::getId).collect(Collectors.toList());
    }
}
